package ias.deepsearch.com.helper.xposed;

import android.util.Log;

import com.google.gson.Gson;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

import de.robv.android.xposed.XC_MethodHook;

/**
 * Created by huaqiancai on 17/10/23.
 */

public class MethodCollector extends XC_MethodHook {
     long start;
     List<TraceLine> buff;
     boolean inited = false;
     String fileName ;
    boolean collect;
    Thread thread;
    static MethodCollector instance;
    private boolean collectArg;

    static class InjectThread extends Thread{

        private final ClassLoader appCl;
      //  private final ClassLoader contextLoader;

        InjectThread (ClassLoader cl){
           appCl = cl;
           // contextLoader = getContextClassLoader();
        }
        @Override
        public void run(){
            for (;;) {
                Log.d("MethodCollector","Try to Inject!");
                try {
                    Thread.sleep(1000);

                    Class commPorter =  loadFromLoader("cn.edu.pku.apiminier.CommPorter");

                           // appCl.loadClass("cn.edu.pku.apiminier.CommPorter");

                    if (commPorter!=null){
                        Field f= commPorter.getDeclaredField("XPOSEDINJECT");
                        f.set(null,MethodCollector.class);
                        Log.d("MethodCollector","InjectMethod done!");
                        return ;
                    }
                } catch (Throwable e) {
                    e.printStackTrace();
                }

            }
        }

        private Class loadFromLoader(String s) {
            try {
                Class ret = getContextClassLoader().loadClass("cn.edu.pku.apiminier.CommPorter");
                return ret;
            } catch (ClassNotFoundException e) {
                try {Class ret2 = null;
                    ret2 = appCl.loadClass("cn.edu.pku.apiminier.CommPorter");
                    return ret2;
                } catch (ClassNotFoundException e1) {
                    e1.printStackTrace();
                }

                e.printStackTrace();
            }
            return null;
        }
    }
    public static MethodCollector getInstance(String pkgName,ClassLoader cl,boolean collectArg){
        if (instance==null)
            instance = new MethodCollector(pkgName,cl,collectArg);
        return instance;
    }
    public MethodCollector(String pkgName,ClassLoader cl,boolean cArg){
        init(pkgName,cl);
        collectArg = cArg;
    }
    public  void init(String pkgName,ClassLoader cl){
        if (inited)
            return;
        thread  = new InjectThread(cl);
        thread.start();
        collect  = true;
        start = System.currentTimeMillis();
        buff = new ArrayList<>();
        fileName = "/sdcard/dumpDex/"+pkgName+"_"+start+".trace";
        try {
           File f  =new File(fileName);
            if (!f.exists()){
               f.createNewFile();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        inited = true;
    }
    public static class TraceLine {
        int threadID;
        int threadClock;
        int wallClock;
        String method;
        int event;
        String retVal;
        List<String> args;
        String exception;
        String field;
        String pre1;
        String pre2;
        public int getEvent() {
            return event;
        }
    }

    @Override
    protected void beforeHookedMethod(XC_MethodHook.MethodHookParam param) throws Throwable {
        if (!collect)
            return;
        TraceLine line = new TraceLine();
        line.threadID = (int)Thread.currentThread().getId();
        line.threadClock =line.wallClock =  (int) (System.currentTimeMillis()-start);
        line.event = 0;
        line.method = param.method.getDeclaringClass().getCanonicalName()+"."+param.method.getName();
        line.args = new ArrayList<>();
        if (collectArg) {
            if (param.args != null)
                for (Object obj : param.args) {
                    if (obj != null)
                        line.args.add(obj.getClass().getCanonicalName());
                    else line.args.add(null);
                }
        }
        StackTraceElement[] stack = Thread.currentThread().getStackTrace();
        if (stack.length>5)
        line.pre1 = stack[5].getClassName()+"."+stack[5].getMethodName();
        if (stack.length>4)
            line.pre2 = stack[4].getClassName()+"."+stack[4].getMethodName();
        collect(line);
    }
    @Override
    protected void afterHookedMethod(XC_MethodHook.MethodHookParam param) throws Throwable {
        if (!collect)
            return;
        TraceLine line = new TraceLine();
        line.threadID = (int)Thread.currentThread().getId();
        line.threadClock =line.wallClock =  (int) (System.currentTimeMillis()-start);
        line.event = 1;
        line.method = param.method.getDeclaringClass().getCanonicalName()+"."+param.method.getName();
        line.args = new ArrayList<>();
        Object result =param.getResult();
        if (collectArg) {
            if (result != null)
                line.retVal = result.getClass().getCanonicalName();
            else line.retVal = "null";
        }
        StackTraceElement[] stack = Thread.currentThread().getStackTrace();
        if (stack.length>5)
            line.pre1 = stack[5].getClassName()+"."+stack[5].getMethodName();
        if (stack.length>4)
            line.pre2 = stack[4].getClassName()+"."+stack[4].getMethodName();
        collect(line);
    }

    private synchronized void collect(TraceLine line) {
        buff.add(line);
        if (buff.size()>10000){
            outputTrace();
        }
    }
    public static String flush(String arg){
        instance.collect = false;
        instance.outputTrace();
        return "success";
    }
    public synchronized void  outputTrace() {
        try {
            FileOutputStream fout = new FileOutputStream(fileName,true);
            Gson g  =new Gson();
            for (TraceLine line:buff) {
                fout.write(g.toJson(line).getBytes());
                fout.write("\n".getBytes());
            }
            buff.clear();
            fout.close();
        }catch(Exception e){

        }
    }
}
